backport: feat(ui): add sync status panel to wallet screen#642
backport: feat(ui): add sync status panel to wallet screen#642
Conversation
…ount Replace hardcoded 3000 duff fee with dynamic fee calculation that accounts for actual number of inputs. Estimates tx size using standard component sizes (P2PKH input ~148B, output ~34B, header ~10B, payload ~60B) and uses max(3000, estimated_size) to always meet the min relay fee. Properly handles fee shortfall when allow_take_fee_from_amount is set, and returns clear error messages for insufficient funds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a manual test plan for the Sync Status Panel; hardens asset-lock and signing code by replacing unwraps with propagated errors and cleaning up on broadcast failure; increases numeric width for a fee log diff; adjusts wallet funding/change flow to handle fee strategies and derive change earlier; refines connection/SPV status formatting; and implements an SPV-aware per-wallet platform_sync_info cache and sync-status panel (dev-mode) with time-ago rendering. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Wallets Screen UI
participant Cache as platform_sync_info Cache
participant DB as Database / Storage
participant SPV as SPV Service
UI->>DB: request platform sync info (seed_hash / wallet id)
DB-->>Cache: return (last_sync_timestamp, last_sync_height)
Cache-->>UI: provide cached platform_sync_info
UI->>SPV: query SpvStatus & SpvSyncProgress (via AppContext)
SPV-->>UI: return status + progress
UI->>UI: format time-ago & spv_phase_summary
UI->>UI: render_sync_status (Core line + Platform line)
Note over UI,DB: on wallet refresh / platform-balance update
UI->>DB: refresh platform sync info
DB-->>Cache: update cached values
Cache-->>UI: re-render sync status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d signed Previously, `asset_lock_transaction_from_private_key` called `take_unspent_utxos_for` which immediately removed selected UTXOs from `Wallet.utxos`. Since fee recalculation and signing happen afterward, any failure at those steps (fee shortfall, missing private key, change address derivation error) would permanently drop UTXOs — especially dangerous in SPV mode where there is no Core RPC reload fallback. Fix: - Add `select_unspent_utxos_for` (`&self`, non-mutating) that performs the same UTXO selection logic without removing anything. - Add `remove_selected_utxos` (`&mut self`) for explicit removal. - Refactor `take_unspent_utxos_for` to delegate to these two methods (no behavior change for existing callers). - In `asset_lock_transaction_from_private_key`, use `select_unspent_utxos_for` for selection and only call `remove_selected_utxos` after the full tx is built and signed. Co-authored-by: lklimek <842586+lklimek@users.noreply.github.com>
# Conflicts: # docs/ai-design/2026-02-24-asset-lock-fee-fix/manual-test-scenarios.md # src/model/wallet/asset_lock_transaction.rs
…ce recalc into remove_selected_utxos Previously, every backend task caller had to manually: (1) remove UTXOs from the in-memory map, (2) drop them from the database, and (3) recalculate affected address balances. This was error-prone — the payment transaction builders were missing the balance recalculation entirely. Now `remove_selected_utxos` accepts an optional `&AppContext` and handles all three steps atomically. The redundant cleanup blocks in 5 backend task callers are removed. Also applies the safe select-then-commit UTXO pattern to `build_standard_payment_transaction` and `build_multi_recipient_payment_transaction`, fixing the same UTXO-loss-on-signing-failure bug that was previously fixed only for asset lock transactions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add checked arithmetic to UTXO selection (amount + fee overflow safety) - Replace hardcoded fee in single-UTXO path with calculate_asset_lock_fee - Add UTXO selection retry when real fee exceeds initial estimate - Document write-lock invariant on select_unspent_utxos_for - Replace .unwrap() with .map_err() on wallet write locks - Restrict Database::shared_connection visibility to pub(crate) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Network directly Replace Option<&AppContext> with concrete dependencies (&Database, Network), removing the need for take_unspent_utxos_for. Extract balance recalculation into a private helper reused by both remove_selected_utxos and the existing AppContext-based wrapper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…extract/sync-status-panel
- Refresh platform sync info cache after wallet refresh completes - Add broadcast failure cleanup in create_asset_lock (remove stale finality tracking entries, replace mutex .unwrap() with .map_err()) - Replace .expect() with proper error propagation in signing loops - Use i128 for fee logging subtraction to prevent overflow - Renumber step comments sequentially after refactoring Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a sync status panel to the wallet screen showing real-time Core and Platform synchronization status. The panel displays Core connection state (RPC connected/disconnected or SPV sync progress with phase-specific percentages) and Platform sync information (address count, sync height, and time since last sync). The PR also includes several bug fixes: improved error handling in asset lock transactions, more descriptive connection status text, corrected comment numbering, and a fix for integer overflow in fee calculation logging.
Changes:
- Adds a compact sync status panel to the wallets screen with Core and Platform status lines, showing RPC/SPV connection state and platform sync details with relative timestamps
- Improves error handling in asset lock transaction building by replacing panics with proper error propagation
- Includes comprehensive manual test scenarios documentation covering 25+ test cases and 6 edge cases
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ui/wallets/wallets_screen/mod.rs | Adds sync status panel rendering, platform sync info caching, time formatting helpers, and integrates with screen lifecycle |
| src/model/wallet/asset_lock_transaction.rs | Replaces expect() panics with proper error handling using ok_or_else() and map_err() |
| src/context/connection_status.rs | Updates status text to be more descriptive (e.g., "Ready" instead of "SPV synced", more detailed endpoint counts) |
| src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs | Fixes step comment numbering from 7-8 to 6-7-8 |
| src/backend_task/identity/top_up_identity.rs | Changes fee difference calculation to use i128 cast to handle negative differences |
| src/backend_task/core/create_asset_lock.rs | Adds broadcast error handling with cleanup of finality tracking and uses map_err for mutex locks |
| docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.md | Comprehensive manual test documentation with 25 test scenarios and 6 edge cases |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
In SPV mode, I keep having |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs (2)
82-86: Inconsistent error handling:lock().unwrap()vslock().map_err()?.Line 84 still uses
lock().unwrap()ontransactions_waiting_for_finality, while the same operation increate_asset_lock.rs(lines 36–39, 93–96) was updated in this PR to uselock().map_err(|e| e.to_string())?. For consistency and to avoid a panic on a poisoned lock, consider aligning this call.Proposed fix
- let mut proofs = self.transactions_waiting_for_finality.lock().unwrap(); + let mut proofs = self + .transactions_waiting_for_finality + .lock() + .map_err(|e| e.to_string())?;Based on learnings, error handling refactoring is needed across the codebase, particularly to avoid panics with
.expect()and instead propagate errors properly using the?operator.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs` around lines 82 - 86, Replace the panic-prone call to transactions_waiting_for_finality.lock().unwrap() with fallible error propagation consistent with other modules: acquire the mutex with transactions_waiting_for_finality.lock().map_err(|e| e.to_string())? and then perform proofs.insert(tx_id, None); so the function (the block in fund_platform_address_from_wallet_utxos where the "Step 2" registration occurs) returns an Err instead of panicking when the lock is poisoned, matching the pattern used in create_asset_lock.rs.
109-118:try_lock()in cleanup may skip cleanup unnecessarily.Line 113 uses
try_lock()which will fail not only on a poisoned lock but also if another thread momentarily holds the mutex. The analogous cleanup increate_asset_lock.rs(lines 52, 105) useslock()which waits for the mutex. Since this is an error-recovery path, it's better to wait briefly rather than risk leaving a stale entry in the finality map.Proposed fix
- if let Ok(mut proofs) = self.transactions_waiting_for_finality.try_lock() { + if let Ok(mut proofs) = self.transactions_waiting_for_finality.lock() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs` around lines 109 - 118, The cleanup path after broadcast_raw_transaction currently uses transactions_waiting_for_finality.try_lock(), which can fail transiently and skip removing tx_id; change it to acquire the mutex with transactions_waiting_for_finality.lock().await (or blocking lock() as used in create_asset_lock.rs) so the code waits for the lock and reliably removes the entry, then proceed to call db.delete_asset_lock_transaction(tx_id.as_byte_array()) and return the error as before; ensure you reference the same mutex (transactions_waiting_for_finality) and tx_id removal logic to mirror the safe cleanup in create_asset_lock.rs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/backend_task/wallet/fund_platform_address_from_wallet_utxos.rs`:
- Around line 82-86: Replace the panic-prone call to
transactions_waiting_for_finality.lock().unwrap() with fallible error
propagation consistent with other modules: acquire the mutex with
transactions_waiting_for_finality.lock().map_err(|e| e.to_string())? and then
perform proofs.insert(tx_id, None); so the function (the block in
fund_platform_address_from_wallet_utxos where the "Step 2" registration occurs)
returns an Err instead of panicking when the lock is poisoned, matching the
pattern used in create_asset_lock.rs.
- Around line 109-118: The cleanup path after broadcast_raw_transaction
currently uses transactions_waiting_for_finality.try_lock(), which can fail
transiently and skip removing tx_id; change it to acquire the mutex with
transactions_waiting_for_finality.lock().await (or blocking lock() as used in
create_asset_lock.rs) so the code waits for the lock and reliably removes the
entry, then proceed to call
db.delete_asset_lock_transaction(tx_id.as_byte_array()) and return the error as
before; ensure you reference the same mutex (transactions_waiting_for_finality)
and tx_id removal logic to mirror the safe cleanup in create_asset_lock.rs.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.mdsrc/backend_task/core/create_asset_lock.rssrc/backend_task/identity/top_up_identity.rssrc/backend_task/wallet/fund_platform_address_from_wallet_utxos.rssrc/context/connection_status.rssrc/model/wallet/asset_lock_transaction.rssrc/ui/wallets/wallets_screen/mod.rs
The PlatformAddressBalances task result handler updated wallet balances but did not refresh the platform_sync_info cache, causing the UI to display "never synced" until the wallet was reselected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Core/Platform sync status panel is now hidden by default and only visible when developer mode is enabled. It uses a collapsible header so developers can expand/collapse it as needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/ui/wallets/wallets_screen/mod.rs`:
- Around line 179-184: The current chain that builds platform_sync_info
(starting from selected_wallet.as_ref().and_then(|w| w.read().ok().map(|g|
g.seed_hash())).and_then(|hash|
app_context.db.get_platform_sync_info(&hash).ok()).map(|(ts, checkpoint,
_terminal)| (ts, checkpoint)).filter(|(ts, _)| *ts > 0)) throws away valid
cached checkpoint data when ts == 0; remove or change the final .filter(|(ts,
_)| *ts > 0) so that platform_sync_info preserves the (ts, checkpoint) tuple
even if ts == 0 (or replace the filter with a check that only discards when both
ts == 0 and checkpoint indicates no data), and update any downstream handling
(e.g., code that renders "Addresses: never synced") to consider checkpoint
presence rather than assuming ts > 0 is required for valid sync info; locate
this logic around selected_wallet, platform_sync_info, seed_hash and
app_context.db.get_platform_sync_info to apply the change.
- Around line 120-121: The platform_sync_info cache is only updated in
select_hd_wallet, causing stale sync state when selected_wallet is reassigned
elsewhere (e.g., post-removal or fallback reselection paths); add a single
helper (e.g., refresh_platform_sync_info or set_selected_wallet) that both
assigns selected_wallet and updates platform_sync_info (using the same logic as
in select_hd_wallet) and replace all direct assignments to selected_wallet
across code paths with calls to this helper so the sync panel always reflects
the newly selected wallet.
🔍 Audit Summary — PR #642Reviewed by: Claude Code with a 3-agent team:
Overall risk: Low. No critical or high-severity issues. The PR improves codebase robustness (panic removal, Mutex error handling) while adding a well-gated diagnostic UI panel. Findings
Pre-existing / Outside-diff observations
✅ Positive observations
Redundancy analysis3 agents produced 18 raw findings. After deduplication: 6 unique actionable findings + 1 pre-existing observation. Redundancy ratio: 39% (7 findings flagged by 2+ agents). Key overlap areas: Mutex handling strategy, SPV phase logic concerns, and time formatting edge cases. 🤖 Reviewed by Claudius the Magnificent AI Agent | |
docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.md
Outdated
Show resolved
Hide resolved
- Centralize wallet selection via set_selected_hd_wallet() helper to keep platform_sync_info cache consistent across all code paths - Add tracing::warn! for Mutex poisoning in asset lock cleanup paths - Fix misleading comment about wallet refresh on broadcast failure - Remove TS-25 from manual test scenarios (not part of this PR) Refs: #657 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consolidate duplicated SPV sync phase formatting into a shared `spv_phase_summary()` function in `connection_status.rs`. The wallet screen now uses this shared function instead of its own copy. The network screen retains its richer operator-facing formatter. The connection indicator tooltip now shows detailed sync progress (e.g. "SPV: Headers: 12345 / 27000 (45%)") instead of bare "SPV: Syncing" when in SPV mode. Also adjust refresh polling rates: 4s when connected, 1s when disconnected (was 10s/2s). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/ui/wallets/wallets_screen/mod.rs (2)
2140-2156:⚠️ Potential issue | 🟠 MajorGuard platform sync cache refresh to the active wallet only.
At Line 2155,
self.refresh_platform_sync_info_cache(&seed_hash)runs even if the async result belongs to a wallet that is no longer selected. That can make the panel show another wallet’s sync timestamp/height after wallet switching.✅ Suggested fix
crate::ui::BackendTaskSuccessResult::PlatformAddressBalances { seed_hash, balances, } => { self.refreshing = false; + let is_selected_wallet = self + .selected_wallet + .as_ref() + .and_then(|w| w.read().ok().map(|g| g.seed_hash())) + .is_some_and(|selected_hash| selected_hash == seed_hash); + // Update wallet's platform_address_info if this is for the selected wallet if let Some(selected) = &self.selected_wallet && let Ok(mut wallet) = selected.write() && wallet.seed_hash() == seed_hash { // Update balances in the wallet for (addr, (balance, nonce)) in balances { wallet.set_platform_address_info(addr, balance, nonce); } } - self.refresh_platform_sync_info_cache(&seed_hash); + if is_selected_wallet { + self.refresh_platform_sync_info_cache(&seed_hash); + } self.set_message( "Successfully synced Platform balances".to_string(), MessageType::Success, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/wallets/wallets_screen/mod.rs` around lines 2140 - 2156, The PlatformAddressBalances branch updates the selected wallet but always calls self.refresh_platform_sync_info_cache(&seed_hash) even if the result no longer corresponds to the currently selected wallet; modify the BackendTaskSuccessResult::PlatformAddressBalances handling so that after verifying the selected wallet (the selected_wallet read lock and wallet.seed_hash() == seed_hash) and updating balances via wallet.set_platform_address_info(...) you only call self.refresh_platform_sync_info_cache(&seed_hash) when that same selected wallet check succeeded (i.e., wrap the refresh call inside the same if-let guard or re-check selected_wallet and seed_hash before calling refresh_platform_sync_info_cache).
12-27:⚠️ Potential issue | 🟡 MinorImport block needs rustfmt cleanup to unblock CI.
The pipeline is currently failing on formatting/import ordering for this file; please run rustfmt on this module.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/wallets/wallets_screen/mod.rs` around lines 12 - 27, The import block in this module is misformatted; run rustfmt (cargo fmt) to fix spacing and ordering and ensure the use statements (e.g., CoreBackendMode, SpvStatus, Component, ConfirmationDialog/ConfirmationStatus, add_left_panel, island_central_panel, add_top_panel, WalletUnlockPopup/WalletUnlockResult, copy_text_to_clipboard, DashColors, AccountCategory/AccountSummary/collect_account_summaries, MessageType/RootScreenType/ScreenLike/ScreenType, DateTime/Utc, spv_phase_summary, Address) are properly grouped and sorted per project style so CI formatting checks pass.src/context/connection_status.rs (1)
333-347:⚠️ Potential issue | 🟡 MinorRefresh-throttle comment is outdated.
Line 333 says “once every 2 seconds,” but current behavior is 4s (synced), 1s (non-synced), and 200ms (SPV stopping).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/context/connection_status.rs` around lines 333 - 347, The comment above the refresh-throttle logic is inaccurate; update the comment to reflect the actual timeout behavior used by the code in the block that reads self.last_update, calls Instant::now(), checks self.spv_status() == SpvStatus::Stopping and self.overall_state() == OverallConnectionState::Synced, and sets timeout to Duration::from_millis(200) for stopping, REFRESH_CONNECTED (4s) when synced, and REFRESH_DISCONNECTED (1s) otherwise; ensure the comment references these three concrete durations and the conditions (SpvStatus::Stopping, OverallConnectionState::Synced) so it stays correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.md`:
- Around line 121-122: Update the described SPV expected outputs to match the
new phase-summary formatter: replace occurrences of the old "Core: Syncing --
Headers NN%" pattern with the new format that shows "current / target (pct%)"
(e.g., "Core: Syncing -- Headers 1234 / 5678 (21%)"), and add coverage for the
new "Masternodes" phase where applicable; update the related entries that still
assume only "NN%" (including the other noted occurrences of the same text) so
all examples and test scenarios show the full "current / target (pct%)" form and
include Masternodes phase output where the app now reports it.
In `@src/backend_task/core/create_asset_lock.rs`:
- Around line 53-57: The new tracing::warn! calls are not rustfmt-compliant;
locate the two uses of tracing::warn! around the
transactions_waiting_for_finality.lock() error paths (the ones that warn "Failed
to clean up finality tracking for tx {tx_id}: Mutex poisoned") and reformat the
macro invocation so the string and arguments are wrapped across lines per
rustfmt (e.g., put the format string on its own line and the variables/arguments
on following indented lines or use a single formatted string via format! before
calling tracing::warn!). Ensure both occurrences use identical, rustfmt-friendly
wrapping and compile without changing the logged message or surrounding logic.
---
Outside diff comments:
In `@src/context/connection_status.rs`:
- Around line 333-347: The comment above the refresh-throttle logic is
inaccurate; update the comment to reflect the actual timeout behavior used by
the code in the block that reads self.last_update, calls Instant::now(), checks
self.spv_status() == SpvStatus::Stopping and self.overall_state() ==
OverallConnectionState::Synced, and sets timeout to Duration::from_millis(200)
for stopping, REFRESH_CONNECTED (4s) when synced, and REFRESH_DISCONNECTED (1s)
otherwise; ensure the comment references these three concrete durations and the
conditions (SpvStatus::Stopping, OverallConnectionState::Synced) so it stays
correct.
In `@src/ui/wallets/wallets_screen/mod.rs`:
- Around line 2140-2156: The PlatformAddressBalances branch updates the selected
wallet but always calls self.refresh_platform_sync_info_cache(&seed_hash) even
if the result no longer corresponds to the currently selected wallet; modify the
BackendTaskSuccessResult::PlatformAddressBalances handling so that after
verifying the selected wallet (the selected_wallet read lock and
wallet.seed_hash() == seed_hash) and updating balances via
wallet.set_platform_address_info(...) you only call
self.refresh_platform_sync_info_cache(&seed_hash) when that same selected wallet
check succeeded (i.e., wrap the refresh call inside the same if-let guard or
re-check selected_wallet and seed_hash before calling
refresh_platform_sync_info_cache).
- Around line 12-27: The import block in this module is misformatted; run
rustfmt (cargo fmt) to fix spacing and ordering and ensure the use statements
(e.g., CoreBackendMode, SpvStatus, Component,
ConfirmationDialog/ConfirmationStatus, add_left_panel, island_central_panel,
add_top_panel, WalletUnlockPopup/WalletUnlockResult, copy_text_to_clipboard,
DashColors, AccountCategory/AccountSummary/collect_account_summaries,
MessageType/RootScreenType/ScreenLike/ScreenType, DateTime/Utc,
spv_phase_summary, Address) are properly grouped and sorted per project style so
CI formatting checks pass.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.mdsrc/backend_task/core/create_asset_lock.rssrc/context/connection_status.rssrc/ui/components/top_panel.rssrc/ui/wallets/wallets_screen/mod.rs
docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.md
Outdated
Show resolved
Hide resolved
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.md
Outdated
Show resolved
Hide resolved
- Update test scenarios TS-07 through TS-11 and TS-23 to reflect new SPV phase format: "Headers: C / T (NN%)" instead of "Headers NN%" - Add Masternodes phase to TS-23 progression - Add developer mode precondition to test scenarios - Fix tooltip showing "syncing..." when SPV is fully synced (Running) - Update stale throttle comment to reflect new refresh rates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>



Summary
format_unix_time_ago()/format_duration_ago()time formatting helpersTest plan
Manual test scenarios:
docs/ai-design/2026-02-24-sync-status-panel/manual-test-scenarios.md🤖 Co-authored by Claudius the Magnificent AI Agent
Summary by CodeRabbit
New Features
Bug Fixes
Documentation